import java.io.IOException;
import java.io.StringWriter;
import java.util.Vector;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;

import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.w3c.dom.*;

/**
 * AnnotationfileHandler is capable of performing multiple operations on an
 * annotation file. It can read some properties from the file, and save them as
 * well.
 * 
 * @author AXXXE
 * 
 */
public class AnnotationfileHandler {
	private Document annotationfile;

	/**
	 * This is the constructor for an AnnotationfileHandler object. It creates
	 * an object on the basis of a filename.
	 * 
	 * @param filename
	 *            The annotation file to be used.
	 * @throws IOException
	 * @throws SAXException
	 * @throws ParserConfigurationException
	 */
	public AnnotationfileHandler(String filename) throws EbFormsException {
		try {
			annotationfile = XMLHandler.readFile(filename);
		} catch (Exception e) {
			throw new EbFormsException("Unable to read annotation file");
		}
	}

	/**
	 * This method obtains a list of supported properties from the annotation
	 * file. It currently looks only in the supported properties in the root of
	 * the document.
	 * 
	 * @return A Vector with the supported properties for this document
	 * @throws Exception
	 *             When the supported properties are not correctly defined.
	 */
	public Vector<String> getSupportedProperties() throws EbFormsException {

		NodeList nlSupportedProperties = annotationfile
				.getElementsByTagName("supported-properties");
		Vector<String> vSupportedProperties = new Vector<String>();

		if (nlSupportedProperties.getLength() == 1) {
			Node n = nlSupportedProperties.item(0);

			Node property = n.getFirstChild();
			while (property != null) {

				if (property.getNodeName().equals("property")) {
					vSupportedProperties.add(property.getTextContent());
				}
				property = property.getNextSibling();
			}
			return vSupportedProperties;

		} else {
			throw new EbFormsException(
					"Supported Properties are not correctly defined in the annotation file!");
		}
	}

	/**
	 * Obtains the allowed values for the property. In the future this should
	 * obtain all allowed properties based on the abstract hierarchy.
	 * 
	 * @param property
	 *            The property for which the allowed values should be obtained.
	 *            The property should be a supported property
	 * @param den
	 *            The core component for which the properties should be
	 *            obtained.
	 * @return The allowed values for the property as defined in the document
	 *         settings. An empty vector if not found.
	 */
	public Vector<String> getAllowedPropertyValues(String sProperty, String den) {

		Vector<String> vHierarchie = XMLHandler.getAbstractHierarchy(den);
		Vector<String> vAllowedValues = new Vector<String>();
		// TODO try-catch block should not be necessary anymore, because all
		// possible nullpointers have been caught already. Remove before
		// release.
		try {
			for (int i = 0; i < vHierarchie.size(); i++) {
				Node nCoreComponent = getElementByDEN(vHierarchie.elementAt(i));
				if (nCoreComponent != null) {
					Node nDocumentSettings = XMLHandler.getChildNodeByName(
							nCoreComponent, "document-settings");
					if (nDocumentSettings != null) {
						Node nProperty = XMLHandler.getChildNodeByName(
								nDocumentSettings, sProperty);
						if (nProperty != null) {
							Node nValue = nProperty.getFirstChild();
							while (nValue != null) {
								if ((nValue.getNodeType() == Node.ELEMENT_NODE)
										&& (!vAllowedValues.contains(nValue
												.getNodeName()))) {

									vAllowedValues.add(nValue.getNodeName());

									// boolean valuefound = false;
									/*
									 * Refactored this bit with the above 2
									 * lines. Easier to read & understand Less
									 * if and for statements. Functionality does
									 * not changes. for (int j = 0; j <
									 * vAllowedValues.size(); j++) { if
									 * (vAllowedValues.get(j).equals(nValue.getNodeName())){
									 * valuefound = true; } } if (!valuefound) {
									 * vAllowedValues.add(nValue.getNodeName()); }
									 */
								}
								nValue = nValue.getNextSibling();
							}
						}
					}
				}
			}

			return vAllowedValues;

		} catch (NullPointerException e) {
			return new Vector<String>();
		}

	}

	/**
	 * This method looks for a notion in the annotationfile, by the name den. If
	 * not found, null is returned.
	 * 
	 * @param den
	 *            The dictionary entry name to lookup.
	 * @return the node containing everything beloning to the core component
	 *         DEN. Null if not found in the annotationfile
	 */
	private Node getElementByDEN(String den) {
		NodeList coreComponents = annotationfile
				.getElementsByTagName("core-component");
		for (int i = 0; i < coreComponents.getLength(); i++) {// Stuurt
																// blijkbaar de
																// lengte terug
																// van alle
																// nodes -1.
			if (coreComponents.item(i).getAttributes().getNamedItem("den")
					.getNodeValue().equals(den)) {
				return coreComponents.item(i);
			}
		}
		return null;
	}

	/**
	 * This methode creates a Core-Component node in the XML tree
	 * 
	 * @param den
	 *            The Core-Component to add
	 * @return Contains the created node
	 */
	private Node createCoreComponent(String den) {
		Element nCoreComponent = annotationfile.createElement("core-component");
		nCoreComponent.setAttribute("den", den);
		return (Node) nCoreComponent;
	}

	/**
	 * This methode creates the corecomponent settings node.
	 * 
	 * @return
	 */
	private Node createComponentSettings() {
		Element nComponentSettings = annotationfile
				.createElement("component-settings");
		return (Node) nComponentSettings;
	}

	/**
	 * This methods reads a stored Core-component's position from the annotation
	 * file
	 * 
	 * @param den
	 *            The Core-component of interest
	 * @return The stored position or -1 if none exists.
	 */
	public int readCCPosition(String den) {
		Node nCoreComponent;
		Node nPosition;

		nCoreComponent = getElementByDEN(den);
		if (nCoreComponent == null)
			return -1;
		else {
			nPosition = XMLHandler.getChildNodeByName(nCoreComponent,
					"position");
			if (nPosition == null)
				return -1;
			else {
				return Integer.decode(nPosition.getTextContent());
			}
		}
	}

	/**
	 * This methods writes a Core-component's position to the annotation file
	 * 
	 * @param den
	 *            The Core-component of interest
	 * @param pos
	 *            The position
	 */
	public void writeCCPosition(String den, int pos) {
		Node nCoreComponent;
		Node nPosition;
		if (den != null && !den.equals("")) {
			nCoreComponent = getElementByDEN(den);
			if (nCoreComponent == null)
				nCoreComponent = annotationfile.getFirstChild().appendChild(
						createCoreComponent(den));
			nPosition = XMLHandler.getChildNodeByName(nCoreComponent,
					"position");
			if (nPosition == null) {
				nPosition = nCoreComponent
						.appendChild(createComponentPosition());
				nPosition.setTextContent(Integer.toString(pos));
			} else {
				nPosition.setTextContent(Integer.toString(pos));
			}
		}
	}

	/**
	 * This method adds a position element to a node.
	 * @return
	 */
	private Node createComponentPosition() {
		Element nComponentPosition = annotationfile.createElement("position");
		return (Node) nComponentPosition;
	}

	/**
	 * This method looks up the given property for the given dictionary entry
	 * name in the annotationfile. It includes the abstract hierarchy as given
	 * by XMLHandler.getAbstractHierarchie()
	 * 
	 * @param property
	 *            The property for which the value should be obtained
	 * @param den
	 *            The DEN for which the property should be obtained.
	 * @return the value for the given property and den. An empty vector if not
	 *         found.
	 */
	public String readProperty(String property, String den) {

		Vector<String> hierarchy = XMLHandler.getAbstractHierarchy(den);
		Node currentComponent;
		Node currentComponentSettings;
		Node nProperty;
		String sPropertyValue = "";

		for (int i = hierarchy.size() - 1; i >= 0; i--) {
			currentComponent = getElementByDEN(hierarchy.elementAt(i));
			if (currentComponent != null) {
				currentComponentSettings = XMLHandler.getChildNodeByName(
						currentComponent, "component-settings");
				if (currentComponentSettings != null) {
					nProperty = XMLHandler.getChildNodeByName(
							currentComponentSettings, property);
					if (nProperty != null) {
						sPropertyValue = nProperty.getAttributes()
								.getNamedItem("value").getNodeValue();
					}
				}
			}
		}
		return sPropertyValue;
	}

	/**
	 * This method enables the user to change a property of a core component
	 * 
	 * @param property
	 *            the property that should be edited
	 * @param newValue
	 *            the new value of the property
	 * @param den
	 *            the core component to edit
	 */
	public void writeProperty(String property, String newValue, String den) {
		Node nCoreComponent;
		Node nComponentSettings;
		Node nProperty;

		nCoreComponent = getElementByDEN(den);
		if (nCoreComponent == null)
			nCoreComponent = annotationfile.getFirstChild().appendChild(
					createCoreComponent(den));
		nComponentSettings = XMLHandler.getChildNodeByName(nCoreComponent,
				"component-settings");
		if (nComponentSettings == null)
			nComponentSettings = nCoreComponent
					.appendChild(createComponentSettings());
		nProperty = XMLHandler.getChildNodeByName(nComponentSettings, property);
		if (nProperty == null) {
			Element element = annotationfile.createElement(property);
			element.setAttribute("value", newValue);
			nComponentSettings.appendChild(element);
		} else {
			nProperty.getAttributes().getNamedItem("value").setNodeValue(
					newValue);
		}
	}

	/**
	 * This method deletes a property from a Core-component.
	 * 
	 * @param property
	 *            The property to remove
	 * @param den
	 *            The Core-component to remove the property from.
	 */
	public void deleteProperty(String property, String den) {
		Node nCoreComponent;
		Node nComponentSettings;
		Node nProperty;

		nCoreComponent = getElementByDEN(den);
		if (nCoreComponent == null)
			return;
		nComponentSettings = XMLHandler.getChildNodeByName(nCoreComponent,
				"component-settings");
		if (nComponentSettings == null)
			return;
		nProperty = XMLHandler.getChildNodeByName(nComponentSettings, property);
		if (nProperty == null)
			return;
		nComponentSettings.removeChild(nProperty);
	}

	/**
	 * Save the annotation file to a given location.
	 * 
	 * @param slocation
	 *            The location on the hard disk the file needs to be saved to.
	 */
	public void saveAnnotationfile(String slocation) throws EbFormsException {
		try {
			Transformer transformer = TransformerFactory.newInstance()
					.newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");

			// initialize StreamResult with File object to save to file
			StreamResult result = new StreamResult(new StringWriter());
			DOMSource source = new DOMSource(annotationfile);
			transformer.transform(source, result);

			String xmlString = result.getWriter().toString();
			java.io.FileWriter writer = new java.io.FileWriter(slocation);
			writer.write(xmlString);
			writer.flush();
			writer.close();
		} catch (Exception ex) {
			throw new EbFormsException("Unable to save annotation file");
		}
	}

	/**
	 * Shows the current state of the annotationfile document in the console
	 * This method is(was) used for debugging only
	 */
	public void showXmlDocument() {
		try {
			Transformer transformer = TransformerFactory.newInstance()
					.newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");

			// initialize StreamResult with File object to save to file
			StreamResult result = new StreamResult(new StringWriter());
			DOMSource source = new DOMSource(annotationfile);
			transformer.transform(source, result);

			String xmlString = result.getWriter().toString();
			System.out.println(xmlString);

		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

}
